/*
 * Created on Dec 6, 2012
 * Created by Paul Gardner
 * 
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License only.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

package com.aelitis.azureus.core.pairing.impl;

import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.security.AlgorithmParameters;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DisplayFormatters;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderFactory;
import org.gudy.azureus2.pluginsimpl.local.utils.resourcedownloader.ResourceDownloaderFactoryImpl;

import com.aelitis.azureus.core.pairing.PairedServiceRequestHandler;

public class PairManagerTunnel {
    private static ResourceDownloaderFactory rdf = ResourceDownloaderFactoryImpl.getSingleton();

    private PairingManagerTunnelHandler tunnel_handler;
    private String tunnel_key;

    private InetAddress originator;
    private String sid;
    private PairedServiceRequestHandler request_handler;
    private SecretKeySpec key;
    private String tunnel_url;
    private String endpoint_url;

    private long last_active = SystemTime.getMonotonousTime();

    private volatile boolean close_requested;

    private final long create_time = SystemTime.getMonotonousTime();

    private long last_request_time;

    private long request_count;
    private long bytes_in;
    private long bytes_out;

    private long last_fail_duration_secs = 0;
    private int consec_fails = 0;

    protected PairManagerTunnel(PairingManagerTunnelHandler _tunnel_handler, String _tunnel_key, InetAddress _originator, String _sid,
            PairedServiceRequestHandler _request_handler, SecretKeySpec _key, String _tunnel_url, String _endpoint_url) {
        tunnel_handler = _tunnel_handler;
        tunnel_key = _tunnel_key;
        originator = _originator;
        sid = _sid;
        request_handler = _request_handler;
        key = _key;
        tunnel_url = _tunnel_url;
        endpoint_url = _endpoint_url;

        new AEThread2("PairManagerTunnel:runner") {
            public void run() {
                try {
                    String current_reply_params = null;
                    byte[] current_reply_data = null;

                    while (!close_requested) {

                        if (consec_fails > 1) {

                            try {
                                Thread.sleep((1 << (consec_fails - 1)) * 1000);

                            } catch (Throwable e) {
                            }
                        }

                        long start_time = SystemTime.getMonotonousTime();

                        try {

                            String url_str = tunnel_url + "?server=true" + (current_reply_params == null ? "" : current_reply_params);

                            if (last_fail_duration_secs > 0) {

                                url_str += "&last_fail=" + last_fail_duration_secs;

                                last_fail_duration_secs = 0;
                            }

                            byte[] bytes_to_send = current_reply_data == null ? new byte[0] : current_reply_data;

                            bytes_out += bytes_to_send.length;

                            ResourceDownloader rd = rdf.create(new URL(url_str), bytes_to_send);

                            rd.setProperty("URL_Connection", "Keep-Alive");
                            rd.setProperty("URL_Read_Timeout", 5 * 60 * 1000);

                            byte[] data = FileUtil.readInputStreamAsByteArray(rd.download());

                            if (close_requested) {

                                break;
                            }

                            bytes_in += data.length;

                            long now = SystemTime.getMonotonousTime();

                            last_active = now;

                            current_reply_params = null;
                            current_reply_data = null;

                            List<String> cookies = (List<String>) rd.getProperty("URL_Set-Cookie");

                            boolean cookie_found = false;

                            if (cookies != null) {

                                for (String cookie : cookies) {

                                    final String name = "vuze_pair_server_reqs=";

                                    if (cookie.startsWith(name)) {

                                        cookie_found = true;

                                        String value = cookie.substring(name.length());

                                        int pos = value.indexOf(';');

                                        value = value.substring(0, pos);

                                        String[] bits = value.split("&");

                                        if (bits.length > 0) {

                                            current_reply_params = "";

                                            int data_pos = 0;

                                            List<byte[]> replies = new ArrayList<byte[]>();
                                            int reply_length = 0;

                                            for (String bit : bits) {

                                                String[] temp = bit.split("=");

                                                if (temp.length == 2) {

                                                    String lhs = temp[0].toLowerCase();

                                                    if (lhs.startsWith("seq")) {

                                                        int seq = Integer.parseInt(lhs.substring(3));
                                                        int len = Integer.parseInt(temp[1]);

                                                        last_request_time = now;

                                                        request_count++;

                                                        byte[] reply = processRequest(data, data_pos, len);

                                                        replies.add(reply);

                                                        reply_length += reply.length;

                                                        data_pos += len;

                                                        current_reply_params += "&seq" + seq + "=" + reply.length;

                                                    } else if (lhs.equals("keepalive")) {

                                                    } else if (lhs.equals("close")) {

                                                        close_requested = true;
                                                    }
                                                }
                                            }

                                            current_reply_data = new byte[reply_length];

                                            data_pos = 0;

                                            for (byte[] reply : replies) {

                                                System.arraycopy(reply, 0, current_reply_data, data_pos, reply.length);

                                                data_pos += reply.length;
                                            }
                                        }
                                    }
                                }
                            }

                            if (!cookie_found) {

                                throw (new Exception("Cookie missing from reply"));
                            }

                            consec_fails = 0;

                        } catch (Throwable e) {

                            long fail_time = SystemTime.getMonotonousTime();

                            last_fail_duration_secs = (fail_time - start_time) / 1000;

                            if (isTimeout(e) && last_fail_duration_secs >= 20) {

                                // 'expected' failure if we're getting dumped on by proxies etc.

                                consec_fails = 0;

                            } else {

                                Debug.out(e);

                                consec_fails++;

                                if (consec_fails > 3) {

                                    break;
                                }
                            }
                        }
                    }
                } finally {

                    tunnel_handler.closeTunnel(PairManagerTunnel.this);
                }
            }
        }.start();
    }

    private boolean isTimeout(Throwable e) {
        if (e == null) {

            return (false);
        }

        if (e instanceof SocketTimeoutException) {

            return (true);
        }

        String message = e.getMessage();

        if (message != null) {

            message = message.toLowerCase(Locale.US);

            if (message.contains("timed out") || message.contains("timeout")) {

                return (true);
            }
        }
        return (isTimeout(e.getCause()));
    }

    private byte[] processRequest(byte[] request, int offset, int length) {
        try {
            byte[] decrypted;

            {
                byte[] IV = new byte[16];

                System.arraycopy(request, offset, IV, 0, IV.length);

                Cipher decipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

                decipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(IV));

                decrypted = decipher.doFinal(request, offset + 16, length - 16);
            }

            byte[] reply_bytes = request_handler.handleRequest(originator, endpoint_url, decrypted);

            {
                Cipher encipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

                encipher.init(Cipher.ENCRYPT_MODE, key);

                AlgorithmParameters params = encipher.getParameters();

                byte[] IV = params.getParameterSpec(IvParameterSpec.class).getIV();

                byte[] enc = encipher.doFinal(reply_bytes);

                byte[] rep_bytes = new byte[IV.length + enc.length];

                System.arraycopy(IV, 0, rep_bytes, 0, IV.length);
                System.arraycopy(enc, 0, rep_bytes, IV.length, enc.length);

                return (rep_bytes);
            }
        } catch (Throwable e) {

            Debug.out(e);

            return (new byte[0]);
        }
    }

    protected String getKey() {
        return (tunnel_key);
    }

    protected long getLastActive() {
        return (last_active);
    }

    protected void destroy() {
        close_requested = true;
    }

    protected String getString() {
        long now = SystemTime.getMonotonousTime();

        return ("url=" + tunnel_url + ", age=" + (now - create_time) + ", last_req="
                + (last_request_time == 0 ? "never" : String.valueOf(now - last_request_time)) + ", reqs=" + request_count + ", in="
                + DisplayFormatters.formatByteCountToKiBEtc(bytes_in) + ", out=" + DisplayFormatters.formatByteCountToKiBEtc(bytes_out)
                + ", lf_secs=" + last_fail_duration_secs + ", consec_fail=" + consec_fails);
    }
}
